﻿#region License
// (c) Intergen.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
#endregion

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.Collections;
using System.Windows;
using System.Windows.Media;

namespace TextGlow.Control.Utilities
{
  public delegate bool PropertyCopyErrorHandler(PropertyInfo property, Exception error);
  public delegate object InstantiationHandler(object currentValue);

  public class Cloner
  {
    public event PropertyCopyErrorHandler HandleError;

    private readonly Dictionary<Type, InstantiationHandler> _typeInstantiationHandlers;
    private readonly List<PropertyInfo> _ignoredProperties;
    private readonly List<object> _clonedHistory;

    public List<PropertyInfo> IgnoredProperties
    {
      get { return _ignoredProperties; }
    }

    public Dictionary<Type, InstantiationHandler> TypeInstantiationHandlers
    {
      get { return _typeInstantiationHandlers; }
    } 

    public Cloner()
    {
      _ignoredProperties = new List<PropertyInfo>();
      _typeInstantiationHandlers = new Dictionary<Type, InstantiationHandler>();
      _clonedHistory = new List<object>();
    }

    private bool SkipCopy(Type t)
    {
      return (t.IsPrimitive
        || t.IsEnum
        || t == typeof(string)
        || t == typeof(Size)
        || t == typeof(Rect)
        || t == typeof(Color)
        || t == typeof(MatrixTransform) /* recursive stack overflow */);
    }

    public object Clone(object source)
    {
      if (source == null)
        return null;

      Type sourceType = source.GetType();

      if (SkipCopy(sourceType))
        return source;

      object cloned;
      try
      {
        InstantiationHandler handler;
        if (_typeInstantiationHandlers.TryGetValue(sourceType, out handler))
          cloned = handler(source);
        else
          cloned = Activator.CreateInstance(sourceType);
      }
      catch (Exception e)
      {
        throw new Exception(string.Format("Error instantiating {0}.", source.GetType()), e);
      }

      CopyValues(source, cloned);

      return cloned;
    }

    private bool IsIgnored(PropertyInfo property)
    {
      return (_ignoredProperties.Where(p => p.DeclaringType == property.DeclaringType
        && p.Name == property.Name).Count() != 0);
    }

    private void CopyValues(object source, object target)
    {
      Type sourceType = source.GetType();

      if (sourceType.IsClass)
      {
        if (_clonedHistory.Contains(source))
          return;
        else
          _clonedHistory.Add(source);
      }


      if (CollectionUtils.IsListType(sourceType))
      {
        IEnumerable sourceEnumerable = (IEnumerable)source;

        try
        {
          foreach (object item in sourceEnumerable)
          {
            target.GetType().GetMethod("Add").Invoke(target, new object[] { Clone(item) });
          }
        }
        catch (Exception ex)
        {
          OnHandleError(target.GetType().GetProperty("Item"), ex);
        }
      }
      else
      {
        List<PropertyInfo> memberInfos = new List<PropertyInfo>(sourceType.GetProperties());

        foreach (PropertyInfo memberInfo in memberInfos)
        {
          if (!IsIgnored(memberInfo) && memberInfo.CanRead)
          {
            try
            {
              object sourceValue = ReflectionUtils.GetMemberValue(memberInfo, source);

              if (_clonedHistory.Contains(sourceValue))
              {
                // do nothing. object is being taken care of. avoid stack overflow
              }
              else if (sourceValue is IEnumerable && !SkipCopy(memberInfo.PropertyType))
              {
                IEnumerable sourceEnumerable = (IEnumerable)sourceValue;
                object targetValue = ReflectionUtils.GetMemberValue(memberInfo, target);

                if (targetValue != null)
                {
                  foreach (object item in sourceEnumerable)
                  {
                    CollectionUtils.Add(targetValue, Clone(item));
                  }
                }
              }
              else if (memberInfo.CanWrite)
              {
                ReflectionUtils.SetMemberValue(memberInfo, target, Clone(sourceValue));
              }
              else
              {
                if (!SkipCopy(memberInfo.PropertyType))
                {
                  object targetValue = ReflectionUtils.GetMemberValue(memberInfo, target);
                  if (targetValue != null)
                    CopyValues(sourceValue, targetValue);
                }
              }
            }
            catch (Exception ex)
            {
              OnHandleError(memberInfo, ex);
            }
          }
        }
      }
    }

    private void OnHandleError(PropertyInfo memberInfo, Exception ex)
    {
      PropertyCopyErrorHandler handleError = HandleError;

      Exception eventException = (ex is TargetInvocationException) ? ex.InnerException : ex;

      if (handleError == null || !handleError(memberInfo, eventException))
        throw new ArgumentException(string.Format("{0} ({1})", "Property copy failed", memberInfo.DeclaringType.FullName + "." + memberInfo.Name), ex);
    }
  }
}